-----------------
-- Tile class
-----------------
Tile = {}
Tile.__index = Tile

function Tile.new(gid, sequence, interval, type)
    local t = {}
    setmetatable(t, Tile)
    
    -- variables
    t.gid = gid or 0
    t.sequence = sequence -- frame sequence
    t.type = type or 0
    t.interval = interval or 0.1
    t.elapsed_time = 0.0
    t.current_index = 0
    t.x = 0.0
    t.y = 0.0
    t.block = false -- solid block
    t.platform = false -- platfrom, can jump through below
    t.diagonal = 0
    
    return t
end

-----------------
-- Set position
-----------------
function Tile:setPosition(x, y)
    self.x = x
    self.y = y
end

-----------------
-- Get position
-----------------
function Tile:getPosition()
    return self.x, self.y
end

-----------------
-- Update tile
-----------------
function Tile:update()
    -- update only if tile has animations
    if self.sequence ~= nil then
        self.elapsed_time = self.elapsed_time + global_delta_time
        if self.elapsed_time > self.interval then
            self.elapsed_time = self.elapsed_time - self.interval
            self.current_index = (self.current_index + 1) % #self.sequence
        end
    end
end

-----------------
-- Get current gid
-----------------
function Tile:getId()
    if self.sequence == nil then
        return self.gid
    else
        return self.sequence[self.current_index + 1] -- Lua array starts with 1
    end
end

-- to string
function Tile:toString()
    return "Tile id:" .. self.gid .. " seqlength:" .. #self.sequence .. " interval:" .. self.interval
end

-----------------
-- Tileset class
-----------------
Tileset = {}
Tileset.__index = Tileset

function Tileset.new(image_src, tilewidth, tileheight)
    local t = {}
    setmetatable(t, Tileset)
    
    -- variables
    t.tilewidth = tilewidth
    t.tileheight = tileheight
    t.sprite = Sprite.new()
    t.sprite:loadImage("undefined", image_src)
    
    return t
end

-----------------
-- Tilelayer class
-----------------
Tilelayer = {}
Tilelayer.__index = Tilelayer

function Tilelayer.new(width, height, data)
    local l = {}
    setmetatable(l, Tilelayer)
    
    -- variables
    l.name = name -- not necessary
    l.width = width
    l.height = height
    l.data = data
    -- l.z_index = z not used anymore because of dictionary
    l.x = 0.0
    l.y = 0.0
    l.old_x = 0.0
    l.old_y = 0.0
    l.scroll = false
    
    return l
end

-----------------
-- Set position
-----------------
function Tilelayer:setPosition(x, y)
    self.x = x
    self.y = y
end

-----------------
-- Get position
----------------
function Tilelayer:getPosition()
    return self.x, self.y
end

-----------------
-- Get position in tiles
----------------
function Tilelayer:getPositionInTiles(tilewidth, tileheight)
    local tx = math.floor(self.x / tilewidth)
    local ty = math.floor(self.y / tileheight)
    
    return tx, ty
end

-----------------
-- Get tile
-----------------
function Tilelayer:getTile(x, y)
    if x >= 0 and x < self.width and y >= 0 and y < self.height then
        return self.data[x + y * self.width + 1]
    else
        return nil
    end
end

-----------------
-- Update
-----------------
function Tilelayer:update(camera)
    local offset_x = 0
    local offsey_y = 0
    
    if camera ~= nil then
        offset_x = camera.x
        offset_y = camera.y
    end
    
    if self.scroll then
        self.x = self.old_x
        self.x = (self.x - 1) % -(self.width * 32)
        self.old_x = self.x
        self.x = self.x + offset_x
    end
end

-----------------
-- Tilemap class
-----------------
Tilemap = {}
Tilemap.__index = Tilemap

function Tilemap.new(width, height, tilewidth, tileheight)
    local t = {}
    setmetatable(t, Tilemap)
    
    -- variables
    t.frame_buffer = love.graphics.newFramebuffer() -- for drawing map without artifacts
    t.width = width
    t.height = height
    t.tilewidth = tilewidth
    t.tileheight = tileheight
    t.layers = {} -- dict
    t.tileset = nil
    
    return t
end

-----------------
-- get tilemap width in pixels
-----------------
function Tilemap:getWidthInPixels()
    return self.width * self.tilewidth
end

-----------------
-- get tilemap height in pixels
-----------------
function Tilemap:getHeightInPixels()
    return self.height * self.tileheight
end

-----------------
-- set position of the layer
-----------------
function Tilemap:setPositionToLayer(index, x, y)
    if self.layers[index] ~= nil then
        self.layers[index]:setPosition(x, y)
    end
end

--[[
-----------------
-- get tile layer
-----------------
function Tilemap:getLayer(index)
    if index >= 1 and index <= #self.layers then
        return self.layers[index]
    else
        return nil
    end
end
--]]

-----------------
-- get layer by name
-----------------
function Tilemap:getLayerByName(name)
	return self.layers[name]
end

-----------------
-- Draw whole map
-----------------
-- function Tilemap:draw(camera)
--     for k, v in pairs(self.layers) do
--         self:drawLayer(k, camera)
--     end
-- end

-----------------
-- Draw single layer from the map
-----------------
function Tilemap:drawLayer(name, camera)
	if self.layers[name] == nil then return end

    -- set frame buffer
    love.graphics.setRenderTarget(self.frame_buffer)
    
    local cur_layer = self.layers[name]
    
    local min_x = math.floor(-cur_layer.x / self.tilewidth)
    local min_y = math.floor(-cur_layer.y / self.tileheight)
    local max_x = math.floor((-cur_layer.x + global_width) / self.tilewidth)
    local max_y = math.floor((-cur_layer.y + global_height) / self.tileheight)

    -- if camera
    if camera ~= nil then
        local left, top, right, bottom = camera:getPosition()
        min_x = math.floor((left - cur_layer.x) / self.tilewidth)
        max_x = math.floor((right - cur_layer.x) / self.tilewidth)
        min_y = math.floor((top - cur_layer.y) / self.tileheight)
        max_y = math.floor((bottom - cur_layer.y) / self.tileheight)
    end
    
    -- if layer is scrollable, double drawing area
    local size = max_x - min_x
    local t_min_x = min_x
    local t_min_y = min_y
    if cur_layer.scroll then
        t_min_x = t_min_x - size
        t_min_y = t_min_y - size
    end
    
    -- if layer is not on the screen, do not draw
    if min_x > cur_layer.width or 
       min_y > cur_layer.height or
       max_x < 0 or max_y < 0
    then 
        -- do nothing
    else
        -- draw only what is seen on the screen
        for ys = t_min_y, max_y do
            for xs = t_min_x, max_x do
                -- id
                local tile = cur_layer:getTile(xs, ys)
                
                if tile ~= nil then
                    local id = tile:getId() - 1
                    
                    if id >= 0 then
                        -- find source positions
                        --local tiles_width = 512 / 32                       
                        local tiles_width = self.tileset.sprite:getWidth() / self.tilewidth
                        local x_pos = math.floor((id % tiles_width))
                        local y_pos = math.floor((id / tiles_width))
                        self.tileset.sprite:setSourceRect(x_pos * self.tilewidth, y_pos * self.tileheight, 
                            self.tilewidth, self.tileheight)
                        
                        -- positions
                        local x_dest = xs * self.tilewidth + cur_layer.x
                        local y_dest = ys * self.tileheight + cur_layer.y
                    
                        -- draw sprite
                        -- if scrollable
                        if cur_layer.scroll then
                        	--[[
                            local other_x = self.tileset.sprite.x + (cur_layer.width * self.tilewidth)
                            
                            if camera ~= nil then
                                --self.tileset.sprite:drawByCamera(camera)
                                self.tileset.sprite:setPosition(other_x, self.tileset.sprite.y)
                                --self.tileset.sprite:drawByCamera(camera)
                            else
                                self.tileset.sprite:draw()
                                self.tileset.sprite:setPosition(other_x, self.tileset.sprite.y)
                                self.tileset.sprite:draw()
                            end
                            --]]
                        else
                            if camera ~= nil then
                                x_dest = camera:getRenderX(x_dest)
                                y_dest = camera:getRenderY(y_dest)
                            end
                            
                            self.tileset.sprite:draw(x_dest, y_dest)
                        end
                    end               
                end
            end
        end
    end
    
    -- set frame buffer to default and draw layers
    love.graphics.setRenderTarget()
    love.graphics.draw(self.frame_buffer, 0, 0, 0, 0.5, 0.5)
end

-----------------
-- Update map
-----------------
function Tilemap:update(camera)
    for k, v in pairs(self.layers) do
        local cur_layer = v
        cur_layer:update(camera)
        
        local min_x = math.floor(-cur_layer.x / self.tilewidth)
        local min_y = math.floor(-cur_layer.y / self.tileheight)
        local max_x = math.floor((-cur_layer.x + global_width) / self.tilewidth)
        local max_y = math.floor((-cur_layer.y + global_height) / self.tileheight)
        
        -- if camera
        if camera ~= nil then
            local left, top, right, bottom = camera:getPosition()
            min_x = math.floor((left - cur_layer.x) / self.tilewidth)
            max_x = math.floor((right - cur_layer.x) / self.tilewidth)
            min_y = math.floor((top - cur_layer.y) / self.tileheight)
            max_y = math.floor((bottom - cur_layer.y) / self.tileheight)
        end
        
        -- if layer is not on the screen, do not update
        if min_x > cur_layer.width or 
           min_y > cur_layer.height or
           max_x < 0 or max_y < 0
        then 
            -- do nothing 
        else
            -- update only which are on the screen
            for ys = min_y, max_y do
                for xs = min_x, max_x do
                    -- id
                    local tile = cur_layer:getTile(xs, ys)
                    if tile ~= nil then
                        -- update tile
                        tile:update()
                    end
                end
            end
        end
    end
end

-----------------
-- Set tileset
-----------------
function Tilemap:setTileset(image_src, tilewidth, tileheight)
    self.tileset = Tileset.new(image_src, tilewidth, tileheight)
end

-----------------
-- Add Layer and sort if necessary
-----------------
function Tilemap:addLayer(name, width, height, data, z)

	--[[
    local z_index = #self.layers + 1
    if z ~= nil then z_index = z end
    table.insert(self.layers, Tilelayer.new(width, height, data, z_index))
    if z ~= nil and z < #self.layers then self:sortLayers() end
    --]]
    
    self.layers[name] = Tilelayer.new(width, height, data)    
end

-----------------
-- Sort layers according to z index
-----------------
function Tilemap:sortLayers()
    table.sort(self.layers, function(a, b) return a.z_index < b.z_index end)
    --print("layers sorted")
end

-- tostring function, TODO
function Tilemap:toString()
    return "Tilemap width: " .. width .. " height " .. height
end
